package org.unidata.mdm.data.service.segments;

import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.type.calculables.CalculableHolder;
import org.unidata.mdm.core.type.data.DataShift;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.formless.BundlesArray;
import org.unidata.mdm.core.type.formless.DataBundle;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.service.impl.CommonRecordsComponent;
import org.unidata.mdm.data.type.calculables.impl.DataRecordHolder;
import org.unidata.mdm.data.type.data.OriginRecord;
import org.unidata.mdm.data.type.data.OriginRecordInfoSection;
import org.unidata.mdm.data.type.data.impl.OriginRecordImpl;
import org.unidata.mdm.data.type.draft.DataDraftConstants;
import org.unidata.mdm.data.type.draft.DataDraftOperation;
import org.unidata.mdm.data.type.keys.RecordEtalonKey;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.data.type.keys.RecordOriginKey;
import org.unidata.mdm.data.type.timeline.RecordTimeline;
import org.unidata.mdm.data.util.StorageUtils;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.draft.type.Edition;

/**
 * @author Mikhail Mikhailov on Sep 26, 2020
 * Draft timeline and keys generation support.
 */
public interface RecordDraftTimelineSupport {
    /**
     * Gets common component, needed for key fetch.
     * @return component
     */
    CommonRecordsComponent commonRecordsComponent();
    /**
     * Generates timeline from record and edition draft.
     * @param draft the draft
     * @param edition the edition
     * @return timeline
     */
    default Timeline<OriginRecord> timeline(Draft draft, @Nullable Edition edition) {

        RecordKeys keys = keys(draft, edition);

        List<CalculableHolder<OriginRecord>> calculables = Collections.emptyList();
        if (Objects.nonNull(edition) && Objects.nonNull(edition.getContent())) {

            BundlesArray bundles = edition.getContent();
            calculables = bundles.stream()
                .map(b -> origin(b, edition, keys))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        }

        return new RecordTimeline(keys, calculables);
    }
    /**
     * Generates or loads the keys, adjusting state according to current operation.
     * @param draft the draft
     * @return keys
     */
    default RecordKeys keys(Draft draft, Edition edition) {
        boolean isNew = BooleanUtils.isTrue(draft.getVariables().valueGet(DataDraftConstants.IS_NEW_RECORD));
        return isNew ? generate(draft, edition) : load(draft);
    }
    /**
     * Generates a calculable holder from a bundle.
     * @param bundle the bundle
     * @param edition the edition
     * @param ok the keys
     * @return holder
     */
    default CalculableHolder<OriginRecord> origin(DataBundle bundle, Edition edition, RecordKeys keys) {

        if (Objects.isNull(bundle)) {
            return null;
        }

        String boxKey = bundle.getVariables().valueGet(DataDraftConstants.BOX_KEY);
        int revision = bundle.getVariables().valueGet(DataDraftConstants.REVISION);
        Instant from = bundle.getVariables().valueGet(DataDraftConstants.VALID_FROM);
        Instant to = bundle.getVariables().valueGet(DataDraftConstants.VALID_TO);
        Instant ts = bundle.getVariables().valueGet(DataDraftConstants.UPDATE_TIMESTAMP);
        OperationType type = bundle.getVariables().valueGet(DataDraftConstants.OPERATION_TYPE, OperationType.class);
        RecordStatus status = bundle.getVariables().valueGet(DataDraftConstants.PERIOD_STATUS, RecordStatus.class);

        Date timestamp = new Date(ts.toEpochMilli());

        OriginRecordImpl origin = new OriginRecordImpl()
            .withDataRecord(bundle.getRecord())
            .withInfoSection(new OriginRecordInfoSection()
                .withOriginKey(keys.findByBoxKey(boxKey))
                .withValidFrom(from == null ? null : new Date(from.toEpochMilli()))
                .withValidTo(to == null ? null : new Date(to.toEpochMilli()))
                .withCreateDate(timestamp)
                .withUpdateDate(timestamp)
                .withCreatedBy(edition.getCreatedBy())
                .withUpdatedBy(edition.getCreatedBy())
                .withRevision(revision)
                .withStatus(status)
                .withOperationType(type)
                .withShift(DataShift.PRISTINE)
                .withMajor(0)
                .withMinor(0));

        return new DataRecordHolder(origin);
    }
    /**
     * Loads exisitng record's keys using draft object.
     * @param draft the draft object
     * @return keys
     */
    default RecordKeys load(Draft draft) {

        RecordKeys k = commonRecordsComponent().identify(RecordEtalonKey.builder()
                    .id(draft.getVariables().valueGet(DataDraftConstants.ETALON_ID))
                    .build());

        // If the draft has no editions saved yet, just return the keys
        // to enable checks against original record state.
        if (!draft.hasEditions()) {
            return k;
        }

        DataDraftOperation operation = draft
                .getVariables()
                .valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);

        switch (operation) {
        case UPSERT_DATA:
        case RESTORE_RECORD:
        case RESTORE_PERIOD:
        case DELETE_PERIOD:

            if (k.getEtalonKey().getStatus() == RecordStatus.ACTIVE) {
                return k;
            }

            return RecordKeys.builder(k)
                    .etalonKey(RecordEtalonKey.builder(k.getEtalonKey())
                            .status(RecordStatus.ACTIVE)
                            .build())
                    .build();
        case DELETE_RECORD:

            if (k.getEtalonKey().getStatus() == RecordStatus.INACTIVE) {
                return k;
            }

            return RecordKeys.builder(k)
                    .etalonKey(RecordEtalonKey.builder(k.getEtalonKey())
                            .status(RecordStatus.INACTIVE)
                            .build())
                    .build();
        default:
            break;
        }

        return k;
    }
    /**
     * Generates phantom keys for a new record using draft object.
     * @param draft the draft object
     * @param edition the edition
     * @return keys
     */
    default RecordKeys generate(Draft draft, @Nullable Edition edition) {

        Date createDate = Objects.nonNull(edition) ? edition.getCreateDate() : new Date();
        String createdBy = Objects.nonNull(edition) ? edition.getCreatedBy() : SecurityUtils.getCurrentUserName();

        // Calculate status even if this is a new record to cover the cases
        // when a new record is "deleted" without publishing.
        DataDraftOperation operation = draft
                .getVariables()
                .valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);

        RecordStatus status = RecordStatus.ACTIVE;
        if (operation == DataDraftOperation.DELETE_RECORD) {
            status = RecordStatus.INACTIVE;
        }

        String etalonId = draft.getVariables().valueGet(DataDraftConstants.ETALON_ID);
        RecordOriginKey rok = RecordOriginKey.builder()
                .createDate(draft.getCreateDate())
                .createdBy(draft.getCreatedBy())
                .updateDate(createDate)
                .updatedBy(createdBy)
                .enrichment(false)
                .entityName(draft.getVariables().valueGet(DataDraftConstants.ENTITY_NAME))
                .externalId(draft.getVariables().valueGet(DataDraftConstants.EXTERNAL_ID))
                .sourceSystem(draft.getVariables().valueGet(DataDraftConstants.SOURCE_SYSTEM))
                .status(RecordStatus.ACTIVE)
                .id(StringUtils.EMPTY)
                .initialOwner(UUID.fromString(etalonId))
                .revision(0)
                .build();

        int shard = draft.getVariables().valueGet(DataDraftConstants.SHARD);
        long lsn = draft.getVariables().valueGet(DataDraftConstants.LSN);
        return RecordKeys.builder()
                .createDate(draft.getCreateDate())
                .createdBy(draft.getCreatedBy())
                .entityName(draft.getVariables().valueGet(DataDraftConstants.ENTITY_NAME))
                .etalonKey(RecordEtalonKey.builder()
                    .id(etalonId)
                    .lsn(lsn)
                    .status(status)
                    .build())
                .originKey(rok)
                .node(StorageUtils.node(shard))
                .published(false)
                .shard(shard)
                .supplementaryKeys(Collections.singletonList(rok))
                .updateDate(createDate)
                .updatedBy(createdBy)
                .build();
    }
}
